﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Xml.Linq;
using System.Data.Linq.Mapping;
using System.Reflection;
using System.Text.RegularExpressions;

namespace LinqToMetaDataAddIn
{
    internal class MetaDataClassGenerator
    {
        private CodeDomProvider codeProvider;
        private CodeNamespace codeNS;
        private XNamespace xNS;

        private string inputFileContent;

        private string dbmlFile;

        public LinkedList<string> FilesGenerated = new LinkedList<string>();

        public string DateFormat { get; set; }

        /// <summary>
        /// Build using the specified language
        /// </summary>
        /// <param name="language">CSharp or VB</param>
        /// <param name="inputFileName">The name (no path) of the original input file</param>
        public MetaDataClassGenerator(string language, string inputFileName)
        {
            this.codeProvider = CodeDomProvider.CreateProvider(language);

            StreamReader sr = new StreamReader(inputFileName);
            this.inputFileContent = sr.ReadToEnd();
            sr.Close();

            this.dbmlFile = inputFileName;
        }



        /// <summary>
        /// Builds the code and returns it as a string
        /// </summary>
        /// <param name="codeNamespace">Namespace to use in the generated code</param>
        /// <returns>Generate the code in a single file or one foe each table</returns>
        public bool BuildCode(string codeNamespace, bool singleFile)
        {
            bool result=true;

            try
            {
                // build the CodeDom tree with the actual contents, place it in 'codeNS' property
                //string classFileName = Path.GetFileName(Path.ChangeExtension(dbmlFile, this.codeProvider.FileExtension));

                if (singleFile)
                {
                    this.BuildCodeDom(codeNamespace);
                }
                else
                {
                    XElement database = XElement.Parse(inputFileContent);
                    xNS = database.Name.Namespace;

                    var tables = database.Elements(xNS + "Table");

                    foreach (XElement table in tables)
                    {
                        string tbname = (string)table.Attribute("Member");
                        this.BuildCode(codeNamespace, tbname);
                         
                    }
                }

                
            }
            catch (Exception ex)
            {
                //result = ex.ToString();
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Build the CodeDom tree
        /// </summary>
        /// <param name="codeNamespace">the namespace where the generated code should belong. If null, the EntityNamespace in the dbml is used</param>
        /// <param name="tableName">specific table name or empty string</param>
        public bool BuildCode(string codeNamespace, string tableName)
        {
            bool result = true;

            try
            {
                CodeGeneratorOptions opt = new CodeGeneratorOptions();
                opt.BlankLinesBetweenMembers = false;
                opt.BracingStyle = "C";

                XElement database = XElement.Parse(inputFileContent);
                xNS = database.Name.Namespace;

                // as we are generating only entities, use the EntityNamespace specified in the dbml
                codeNS = new CodeNamespace(((string)database.Attribute("EntityNamespace")) ?? codeNamespace);

                this.EmitHeader(database);

                var tables = (from t in database.Elements(xNS + "Table")
                             where (string)t.Attribute("Member")  == tableName
                             select t).FirstOrDefault();

                this.EmitTable(tables);

                string classFileName = tableName + "." + this.codeProvider.FileExtension;

                this.WriteClassFile(classFileName);

            }
            catch (Exception ex)
            {
                //result = ex.ToString();
                result = false;
            }

            return result;
        }


        /// <summary>
        /// Build the CodeDom tree
        /// </summary>
        /// <param name="codeNamespace">the namespace where the generated code should belong. If null, the EntityNamespace in the dbml is used</param>
        private void BuildCodeDom(string codeNamespace)
        {
            CodeGeneratorOptions opt = new CodeGeneratorOptions();
            opt.BlankLinesBetweenMembers = false;
            opt.BracingStyle = "C";

            XElement database = XElement.Parse(inputFileContent);
            xNS = database.Name.Namespace;

            // as we are generating only entities, use the EntityNamespace specified in the dbml
            codeNS = new CodeNamespace(((string)database.Attribute("EntityNamespace")) ?? codeNamespace);
           
           this.EmitHeader(database);  

           var tables = database.Elements(xNS + "Table");

            foreach (XElement table in tables)
            {
                this.EmitTable(table);
            }

            string classFileName = Path.GetFileName(Path.ChangeExtension(dbmlFile, this.codeProvider.FileExtension));
            
            this.WriteClassFile(classFileName);
 
        }

        private void WriteClassFile(string classFileName)
        {

            string contentFile = string.Empty;

            using (StringWriter sw = new StringWriter())
            {
                CodeGeneratorOptions opt = new CodeGeneratorOptions();
                opt.BlankLinesBetweenMembers = false;
                opt.BracingStyle = "C";

                codeProvider.GenerateCodeFromNamespace(codeNS, sw, opt);
                sw.Flush();

                contentFile = sw.ToString();

                //bug for c#
                //Replace get {} (bug) whit get;

                string tabs;
                for (int t = 1; t < 4; t++)
                {
                    tabs = new string(' ', 4 * t);
                    string sget = "get\r\n" + tabs + "{\r\n" + tabs + "}\r\n";
                    string sset = "set\r\n" + tabs + "{\r\n" + tabs + "}";

                    contentFile = contentFile.Replace(sget, "get;");
                    contentFile = contentFile.Replace(sset, "set;");
                }
            }


            classFileName = Path.Combine(Path.GetTempPath(), classFileName);

            FileStream fs = File.Create(classFileName);
            StreamWriter sw1 = new StreamWriter(fs);
            sw1.Write(contentFile);
            sw1.Flush();
            sw1.Close();
            fs.Close();

            this.FilesGenerated.AddLast(classFileName);

        }


        /// <summary>
        /// Builds the current CodeDom CodeNamespace header,
        /// including comment and using statements
        /// </summary>
        /// <param name="database"></param>
        private void EmitHeader(XElement database)
        {
            codeNS.Comments.Add(new CodeCommentStatement(String.Format(
@"------------------------------------------------------------------------------
<auto-generated>
    This code was generated by a tool at {0}
    Tool Name: LinqToMetaDataGenerator
    Tool Version: {1}
    Runtime Version: {2}

    Changes to this file may cause incorrect behavior and will be lost if
    the code is regenerated.
</auto-generated>
------------------------------------------------------------------------------",
                                                                               DateTime.Now.ToShortDateString(),
                System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
                System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())));

            codeNS.Imports.Add(new CodeNamespaceImport("System"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Data"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Data.Linq"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Data.Linq.Mapping"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Linq"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Linq.Expressions"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.Runtime.Serialization"));
            codeNS.Imports.Add(new CodeNamespaceImport("System.ComponentModel.DataAnnotations"));
        }

        /// <summary>
        /// Builds the CodeDom declaration of a Table
        /// </summary>
        /// <param name="table">a table element from a dbml file</param>
        private void EmitTable(XElement table)
        {
            string className = (string)table.Attribute("Member");
            XElement type = table.Element(xNS + "Type");
            
            CodeTypeDeclaration cls = new CodeTypeDeclaration();
            cls.Name = className;
            cls.IsClass = true;
            cls.IsPartial = true;
            cls.TypeAttributes = TypeAttributes.NestedPublic;

            string classMetaData = className + "MetaData";
            CodeAttributeArgument mt = new CodeAttributeArgument("", new CodeTypeOfExpression(classMetaData));
            cls.CustomAttributes.Add(new CodeAttributeDeclaration("MetadataType", mt));
            codeNS.Types.Add(cls);


            // table class declaration
            CodeTypeDeclaration ctd = new CodeTypeDeclaration(classMetaData);

            ctd.IsPartial = true;
            ctd.IsClass = true;
            ctd.TypeAttributes = TypeAttributes.Public;

            //Association
            var ass = from c in type.Elements(xNS + "Association")
                      select new
                      {
                          Name = (string)c.Attribute("Name"),
                          Type = (string)c.Attribute("Type"),
                          Member = (string)c.Attribute("Member"),
                          IsForeignKey = (bool?)c.Attribute("IsForeignKey") ?? false,
                          IsAssociation = (bool?)c.Attribute("Association") ?? false
                      };


            //// emit the public property for each Foreign Association
            int i = 0;
            foreach (var col in ass)
            {
                if (col.IsForeignKey)
                {
                    string colName = col.Member.Replace("[", "").Replace("]", "");

                    CodeTypeReference colTypeRef = new CodeTypeReference(col.Type);
                    CodeMemberProperty prop = new CodeMemberProperty();

                    //Display(Order=0)
                    CodeAttributeArgument dn = new CodeAttributeArgument("", new CodePrimitiveExpression(this.ToTitleFromPascal(colName)));
                    prop.CustomAttributes.Add(new CodeAttributeDeclaration("DisplayName", dn));

                    dn = new CodeAttributeArgument("Order", new CodePrimitiveExpression(i));
                    prop.CustomAttributes.Add(new CodeAttributeDeclaration("Display", dn));

                    //Scaffold
                    CodeAttributeArgument caa = new CodeAttributeArgument("", new CodePrimitiveExpression(true));
                    prop.CustomAttributes.Add(new CodeAttributeDeclaration("ScaffoldColumn", caa));

                    // property definition
                    prop.Name = colName;
                    prop.Type = colTypeRef;
                    prop.HasGet = true;
                    prop.HasSet = true;
                    prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                    ctd.Members.Add(prop);
                    ++i;
                }
            }


            // get columns data
            //.Elements(xNS + "Column" )
            var columns = from c in type.Elements(xNS + "Column")
                          select new
                          {
                              Name = (string)c.Attribute("Name"),
                              Type = (string)c.Attribute("Type"),
                              DbType = (string)c.Attribute("DbType"),
                              AutoSync = (string)c.Attribute("AutoSync"),
                              IsPrimaryKey = (bool?)c.Attribute("IsPrimaryKey") ?? false,
                              IsDbGenerated = (bool?)c.Attribute("IsDbGenerated") ?? false,
                              IsVersion = (bool?)c.Attribute("IsVersion") ?? false,
                              CanBeNull = (bool?)c.Attribute("CanBeNull") ?? false,
                              UpdateCheck = (string)c.Attribute("UpdateCheck")
                   
                          };

            //// emit the public property for each column
            foreach (var col in columns)
            {
                string colName = col.Name.Replace("[", "").Replace("]", "");

                // replace value types with CanBeNull parameter set with Nullable Types
                CodeTypeReference colTypeRef = new CodeTypeReference(col.Type);
                if (Type.GetType(col.Type).IsValueType && col.CanBeNull)
                    colTypeRef = new CodeTypeReference("System.Nullable", colTypeRef);

                CodeMemberProperty prop = new System.CodeDom.CodeMemberProperty();

                //Display(Order=x)
                string displayName = this.ToTitleFromPascal(colName);
                CodeAttributeArgument dn = new CodeAttributeArgument("", new CodePrimitiveExpression(displayName));
                prop.CustomAttributes.Add(new CodeAttributeDeclaration("DisplayName", dn));

                dn = new CodeAttributeArgument("Order", new CodePrimitiveExpression(i));
                prop.CustomAttributes.Add(new CodeAttributeDeclaration("Display", dn));

                //Scaffold
                bool scf = !displayName.ToUpper().StartsWith("ID ");
                CodeAttributeArgument caa = new CodeAttributeArgument("", new CodePrimitiveExpression(scf));
                prop.CustomAttributes.Add(new CodeAttributeDeclaration("ScaffoldColumn", caa));

                //DisplayFormat
                this.DisplayFormatAttribute(ref prop, Type.GetType(col.Type));
                
                // property definition
                prop.Name = colName;
                prop.Type = colTypeRef;
                prop.HasGet = true;
                prop.HasSet = true;
                prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                ctd.Members.Add(prop);
                ++i;
            }

            codeNS.Types.Add(ctd);
        }

        private void DisplayFormatAttribute(ref CodeMemberProperty property, Type type)
        {
            string displayFormat = "{0}";
            bool inEditMode = false;

            if (type == typeof(DateTime) || type == typeof(Nullable<DateTime>))
            {
                if (string.IsNullOrEmpty(this.DateFormat))
                {
                    displayFormat = "{0:d}";    
                }
                else
                {
                    displayFormat =  this.DateFormat  ;
                }

                inEditMode = true;
            }
            else if (type == typeof(decimal) || type == typeof(Nullable<decimal>))
            {
                displayFormat = "{0:c}";
            }

            //string displayFormat = this.GetDisplayFormat(type);

            if (!displayFormat.Equals("{0}"))
            {
                CodeAttributeArgument[] df = { new CodeAttributeArgument(), new CodeAttributeArgument() };

                df[0] = new CodeAttributeArgument("DataFormatString", new CodePrimitiveExpression(displayFormat));
                df[1] = new CodeAttributeArgument("ApplyFormatInEditMode", new CodePrimitiveExpression(inEditMode));
                property.CustomAttributes.Add(new CodeAttributeDeclaration("DisplayFormat", df));
            }
        }

        /// <summary>
        /// Converts a Pascal type Name to Title
        /// i.e. MyEmployees becomes My Employees
        ///      OrderID becomes Order ID etc
        /// </summary>
        /// <param name="s">String to convert</param>
        /// <returns>Title String</returns>
        public String ToTitleFromPascal(string s)
        {
            // remove name space
            String s0 = Regex.Replace(s, "(.*\\.)(.*)", "$2");
            // add space before Capital letter
            String s1 = Regex.Replace(s0, "[A-Z]", " $&");

            // replace '_' with space
            String s2 = Regex.Replace(s1, "[_]", " ");

            // replace double space with single space
            String s3 = Regex.Replace(s2, "  ", " ");

            // remove and double capitals with inserted space
            String s4 = Regex.Replace(s3, "(?<before>[A-Z])\\s(?<after>[A-Z])", "${before}${after}");

            // remove and double capitals with inserted space
            String sf = Regex.Replace(s4, "^\\s", "");

            // force first character to upper case
            return  this.ToTitleCase(sf);
        }

        private string ToTitleCase(string text)
        {
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < text.Length; i++)
            {
                if (i > 0)
                {
                    if (text.Substring(i - 1, 1) == " " ||
                        text.Substring(i - 1, 1) == "\t" ||
                        text.Substring(i - 1, 1) == "/")
                        sb.Append(text.Substring(i, 1).ToString().ToUpper());
                    else
                        sb.Append(text.Substring(i, 1).ToString().ToLower());
                }
                else
                    sb.Append(text.Substring(i, 1).ToString().ToUpper());
            }

            return sb.ToString();
        }

        private string GetDisplayFormat(Type type)
        {
            string defaultFormat = "{0}";

            if (type == typeof(DateTime) || type == typeof(Nullable<DateTime>))
            {
                defaultFormat = "{0:d}";
            }
            else if (type == typeof(decimal) || type == typeof(Nullable<decimal>))
            {
                defaultFormat = "{0:c}";
            }

            return defaultFormat;
        }

    }
}
